﻿#region Copyright (c) 2013-03, Olaf Kober <amarok.projects@gmail.com>
//================================================================================
//	Permission is hereby granted, free of charge, to any person obtaining a copy
//	of this software and associated documentation files (the "Software"), to deal
//	in the Software without restriction, including without limitation the rights
//	to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//	copies of the Software, and to permit persons to whom the Software is
//	furnished to do so, subject to the following conditions:
//
//	The above copyright notice and this permission notice shall be included in
//	all copies or substantial portions of the Software.
//
//	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//	IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//	FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//	AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//	LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//	OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//	THE SOFTWARE.
//================================================================================
#endregion

#if TESTS

using System;
using System.Threading;
using System.Threading.Tasks;
using NUnit.Framework;


namespace Amarok.Agents
{
	[TestFixture]
	public class Test_Runtime
	{
		private class MockSubsystem : Subsystem
		{
			public static RuntimeEnvironment EnvironmentSupplied;
			public static Int32 OnLoadCalled;
			public static Int32 OnCleanupCalled;

			public MockSubsystem(RuntimeEnvironment environment)
				: base(environment)
			{
				EnvironmentSupplied = environment;
			}

			protected override void OnLoad()
			{
				base.OnLoad();
				OnLoadCalled++;
			}

			protected override void OnCleanup()
			{
				base.OnCleanup();
				OnCleanupCalled++;
			}
		}

		private class MockRuntime : Runtime<RuntimeEnvironment>
		{
			public IMessageBus MessageBusCreated;
			public RuntimeEnvironment EnvironmentCreated;
			public Int32 OnStartupCalled;
			public Int32 OnCleanupCalled;
			public Boolean ShouldAutoShutdown;
			public Boolean ThrowInCreateMessageBus;
			public Boolean ThrowInCreateEnvironment;
			public Boolean ThrowInOnStartup;
			public Boolean ThrowInOnCleanup;
			public Boolean ThrowRuntimeStartExceptionInOnStartup;


			protected override IMessageBus CreateMessageBus()
			{
				if (ThrowInCreateMessageBus)
					throw new ApplicationException("CREATEMESSAGEBUS");

				MessageBusCreated = base.CreateMessageBus();
				Assert.That(MessageBusCreated, Is.Not.Null);

				return MessageBusCreated;
			}

			protected override RuntimeEnvironment CreateEnvironment(IRuntime runtime, IMessageBus messageBus)
			{
				if (ThrowInCreateEnvironment)
					throw new ApplicationException("CREATEENVIRONMENT");

				Assert.That(runtime, Is.Not.Null);
				Assert.That(messageBus, Is.Not.Null);
				Assert.That(messageBus, Is.SameAs(MessageBusCreated));

				EnvironmentCreated = new RuntimeEnvironment(runtime, messageBus);
				return EnvironmentCreated;
			}

			protected override void OnStartup()
			{
				if (ThrowInOnStartup)
					throw new ApplicationException("ONSTARTUP");
				if (ThrowRuntimeStartExceptionInOnStartup)
					throw new RuntimeStartException("ONSTARTUP");

				base.OnStartup();

				OnStartupCalled++;

				base.LoadSubsystem(typeof(MockSubsystem), base.Environment);
				base.LoadSubsystem(typeof(MockSubsystem));
				base.LoadSubsystem<MockSubsystem>(base.Environment);
				base.LoadSubsystem<MockSubsystem>();
				base.LoadSubsystem<MockSubsystem, RuntimeEnvironment>(base.Environment);

				if (ShouldAutoShutdown)
				{
					Task.Factory
						.StartNew(() => base.Environment.Runtime.Shutdown())
						.IgnoreExceptions();
				}
			}

			protected override void OnCleanup()
			{
				if (ThrowInOnCleanup)
					throw new ApplicationException("ONCLEANUP");

				base.OnCleanup();

				OnCleanupCalled++;
			}

			public ISubsystem TestLoadSubsystem(Type subsystemType)
			{
				return base.LoadSubsystem(subsystemType);
			}
		}

		private class TestMessage : Message
		{
		}
		private class TestOperation : Operation<None>
		{
		}


		[SetUp]
		public void TestSetup()
		{
			MockSubsystem.OnLoadCalled = 0;
			MockSubsystem.OnCleanupCalled = 0;
			MockSubsystem.EnvironmentSupplied = null;
		}


		// --- TESTS ---


		[Test]
		public void Test_Construction()
		{
			using (var runtime = new MockRuntime())
			{
				Assert.That(runtime.HasStarted, Is.False);
				Assert.That(runtime.OnStartupCalled, Is.EqualTo(0));
				Assert.That(runtime.OnCleanupCalled, Is.EqualTo(0));
				Assert.That(runtime.MessageBusCreated, Is.Null);
				Assert.That(runtime.EnvironmentCreated, Is.Null);
				Assert.That(MockSubsystem.OnLoadCalled, Is.EqualTo(0));
				Assert.That(MockSubsystem.OnCleanupCalled, Is.EqualTo(0));
				Assert.That(MockSubsystem.EnvironmentSupplied, Is.Null);

				Assert.Throws<InvalidOperationException>(() =>
					{
						var environment = runtime.Environment;
					});
				Assert.Throws<InvalidOperationException>(() =>
					{
						runtime.Publish(new TestMessage());
					});
				Assert.Throws<InvalidOperationException>(() =>
					{
						runtime.Publish("AAA", new TestMessage());
					});
				Assert.Throws<InvalidOperationException>(() =>
					{
						runtime.PublishOperation(new TestOperation());
					});
				Assert.Throws<InvalidOperationException>(() =>
					{
						runtime.PublishOperation("AAA", new TestOperation());
					});
				Assert.Throws<InvalidOperationException>(() =>
					{
						runtime.PublishOperation(new TestOperation(), CancellationToken.None);
					});
				Assert.Throws<InvalidOperationException>(() =>
					{
						runtime.PublishOperation("AAA", new TestOperation(), CancellationToken.None);
					});
				Assert.Throws<InvalidOperationException>(() =>
					{
						runtime.Wait();
					});
				Assert.Throws<InvalidOperationException>(() =>
					{
						runtime.Wait(1000);
					});
				Assert.Throws<InvalidOperationException>(() =>
					{
						runtime.Wait(TimeSpan.FromMilliseconds(1000));
					});
			}
		}

		[Test]
		public void Test_Start_Shutdown()
		{
			using (var runtime = new MockRuntime())
			{
				// --- instantiated
				Assert.That(runtime.HasStarted, Is.False);
				Assert.That(runtime.OnStartupCalled, Is.EqualTo(0));
				Assert.That(runtime.OnCleanupCalled, Is.EqualTo(0));
				Assert.That(runtime.MessageBusCreated, Is.Null);
				Assert.That(runtime.EnvironmentCreated, Is.Null);
				Assert.That(MockSubsystem.OnLoadCalled, Is.EqualTo(0));
				Assert.That(MockSubsystem.OnCleanupCalled, Is.EqualTo(0));
				Assert.That(MockSubsystem.EnvironmentSupplied, Is.Null);

				// --- started
				runtime.Start();

				Assert.That(runtime.HasStarted, Is.True);
				Assert.That(runtime.OnStartupCalled, Is.EqualTo(1));
				Assert.That(runtime.OnCleanupCalled, Is.EqualTo(0));
				Assert.That(runtime.MessageBusCreated, Is.Not.Null);
				Assert.That(runtime.EnvironmentCreated, Is.Not.Null);
				Assert.That(runtime.Environment, Is.SameAs(runtime.EnvironmentCreated));
				Assert.That(runtime.Environment.Runtime, Is.SameAs(runtime));
				Assert.That(runtime.Environment.MessageBus, Is.SameAs(runtime.MessageBusCreated));
				Assert.That(MockSubsystem.OnLoadCalled, Is.EqualTo(5));
				Assert.That(MockSubsystem.OnCleanupCalled, Is.EqualTo(0));
				Assert.That(MockSubsystem.EnvironmentSupplied, Is.SameAs(runtime.EnvironmentCreated));

				Assert.Throws<InvalidOperationException>(() =>
					{
						runtime.Start();
					});
				Assert.Throws<InvalidOperationException>(() =>
					{
						runtime.Run();
					});

				runtime.Publish(new TestMessage());
				runtime.Publish("ABC", new TestMessage());
				runtime.PublishOperation(new TestOperation());
				runtime.PublishOperation("ABC", new TestOperation());

				Assert.That(runtime.Wait(10), Is.False);
				Assert.That(runtime.Wait(TimeSpan.FromMilliseconds(10)), Is.False);

				// --- shut down
				runtime.Shutdown();
				runtime.Wait();

				Assert.That(runtime.HasStarted, Is.True);
				Assert.That(runtime.OnStartupCalled, Is.EqualTo(1));
				Assert.That(runtime.OnCleanupCalled, Is.EqualTo(1));
				Assert.That(runtime.MessageBusCreated, Is.Not.Null);
				Assert.That(runtime.EnvironmentCreated, Is.Not.Null);
				Assert.That(runtime.Environment, Is.SameAs(runtime.EnvironmentCreated));
				Assert.That(runtime.Environment.Runtime, Is.SameAs(runtime));
				Assert.That(runtime.Environment.MessageBus, Is.SameAs(runtime.MessageBusCreated));
				Assert.That(MockSubsystem.OnLoadCalled, Is.EqualTo(5));
				Assert.That(MockSubsystem.OnCleanupCalled, Is.EqualTo(5));
				Assert.That(MockSubsystem.EnvironmentSupplied, Is.SameAs(runtime.EnvironmentCreated));

				Assert.Throws<InvalidOperationException>(() =>
					{
						runtime.Start();
					});
				Assert.Throws<InvalidOperationException>(() =>
					{
						runtime.Run();
					});

				runtime.Publish(new TestMessage());
				runtime.Publish("ABC", new TestMessage());
				runtime.PublishOperation(new TestOperation());
				runtime.PublishOperation("ABC", new TestOperation());

				runtime.Wait();
				Assert.That(runtime.Wait(10), Is.True);
				Assert.That(runtime.Wait(TimeSpan.FromMilliseconds(10)), Is.True);
			}
		}

		[Test]
		public void Test_Start_Shutdown_Via_IRuntime()
		{
			using (var runtime = new MockRuntime())
			{
				// --- instantiated
				// --- started
				runtime.Start();

				// --- shut down
				runtime.Environment.Runtime.Shutdown();
				runtime.Wait();

				Assert.That(runtime.HasStarted, Is.True);
				Assert.That(runtime.OnStartupCalled, Is.EqualTo(1));
				Assert.That(runtime.OnCleanupCalled, Is.EqualTo(1));
				Assert.That(runtime.MessageBusCreated, Is.Not.Null);
				Assert.That(runtime.EnvironmentCreated, Is.Not.Null);
				Assert.That(runtime.Environment, Is.SameAs(runtime.EnvironmentCreated));
				Assert.That(runtime.Environment.Runtime, Is.SameAs(runtime));
				Assert.That(runtime.Environment.MessageBus, Is.SameAs(runtime.MessageBusCreated));
				Assert.That(MockSubsystem.OnLoadCalled, Is.EqualTo(5));
				Assert.That(MockSubsystem.OnCleanupCalled, Is.EqualTo(5));
				Assert.That(MockSubsystem.EnvironmentSupplied, Is.SameAs(runtime.EnvironmentCreated));

				Assert.Throws<InvalidOperationException>(() =>
					{
						runtime.Start();
					});
				Assert.Throws<InvalidOperationException>(() =>
					{
						runtime.Run();
					});

				runtime.Publish(new TestMessage());
				runtime.Publish("ABC", new TestMessage());
				runtime.PublishOperation(new TestOperation());
				runtime.PublishOperation("ABC", new TestOperation());

				runtime.Wait();
				Assert.That(runtime.Wait(10), Is.True);
				Assert.That(runtime.Wait(TimeSpan.FromMilliseconds(10)), Is.True);
			}
		}

		[Test]
		public void Test_Start_AfterShutdown()
		{
			using (var runtime = new MockRuntime())
			{
				runtime.Shutdown();

				Assert.Throws<InvalidOperationException>(() =>
					{
						runtime.Start();
					});
			}
		}

		[Test]
		public void Test_Start_Twice()
		{
			using (var runtime = new MockRuntime())
			{
				runtime.Start();

				Assert.Throws<InvalidOperationException>(() =>
					{
						runtime.Start();
					});
			}
		}

		[Test]
		public void Test_Start_Exception_In_CreateMessageBus()
		{
			using (var runtime = new MockRuntime())
			{
				runtime.ThrowInCreateMessageBus = true;

				var exception = Assert.Throws<RuntimeStartException>(() =>
					{
						runtime.Start();
					});

				Assert.That(exception.InnerException, Is.TypeOf<ApplicationException>());
				Assert.That(exception.InnerException.Message, Is.EqualTo("CREATEMESSAGEBUS"));

				runtime.Shutdown();
				runtime.Wait();
			}
		}

		[Test]
		public void Test_Start_Exception_In_CreateEnvironment()
		{
			using (var runtime = new MockRuntime())
			{
				runtime.ThrowInCreateEnvironment = true;

				var exception = Assert.Throws<RuntimeStartException>(() =>
					{
						runtime.Start();
					});

				Assert.That(exception.InnerException, Is.TypeOf<ApplicationException>());
				Assert.That(exception.InnerException.Message, Is.EqualTo("CREATEENVIRONMENT"));

				runtime.Shutdown();
				runtime.Wait();
			}
		}

		[Test]
		public void Test_Start_Exception_In_OnStartup()
		{
			using (var runtime = new MockRuntime())
			{
				runtime.ThrowInOnStartup = true;

				var exception = Assert.Throws<RuntimeStartException>(() =>
					{
						runtime.Start();
					});

				Assert.That(exception.InnerException, Is.TypeOf<ApplicationException>());
				Assert.That(exception.InnerException.Message, Is.EqualTo("ONSTARTUP"));

				runtime.Shutdown();
				runtime.Wait();
			}
		}

		[Test]
		public void Test_Start_RuntimeStartException_In_OnStartup()
		{
			using (var runtime = new MockRuntime())
			{
				runtime.ThrowRuntimeStartExceptionInOnStartup = true;

				var exception = Assert.Throws<RuntimeStartException>(() =>
					{
						runtime.Start();
					});

				Assert.That(exception.InnerException, Is.Null);
				Assert.That(exception.Message, Is.EqualTo("ONSTARTUP"));

				runtime.Shutdown();
				runtime.Wait();
			}
		}

		[Test]
		public void Test_Start_Exception_In_OnCleanup()
		{
			using (var runtime = new MockRuntime())
			{
				runtime.ThrowInOnCleanup = true;

				runtime.Start();
				runtime.Shutdown();
				runtime.Wait();
			}
		}

		[Test]
		public void Test_Run()
		{
			using (var runtime = new MockRuntime())
			{
				// --- instantiated
				Assert.That(runtime.HasStarted, Is.False);
				Assert.That(runtime.OnStartupCalled, Is.EqualTo(0));
				Assert.That(runtime.OnCleanupCalled, Is.EqualTo(0));
				Assert.That(runtime.MessageBusCreated, Is.Null);
				Assert.That(runtime.EnvironmentCreated, Is.Null);
				Assert.That(MockSubsystem.OnLoadCalled, Is.EqualTo(0));
				Assert.That(MockSubsystem.OnCleanupCalled, Is.EqualTo(0));
				Assert.That(MockSubsystem.EnvironmentSupplied, Is.Null);


				// --- started
				// --- shut down
				runtime.ShouldAutoShutdown = true;
				runtime.Run();
				runtime.Wait();

				Assert.That(runtime.HasStarted, Is.True);
				Assert.That(runtime.OnStartupCalled, Is.EqualTo(1));
				Assert.That(runtime.OnCleanupCalled, Is.EqualTo(1));
				Assert.That(runtime.MessageBusCreated, Is.Not.Null);
				Assert.That(runtime.EnvironmentCreated, Is.Not.Null);
				Assert.That(runtime.Environment, Is.SameAs(runtime.EnvironmentCreated));
				Assert.That(runtime.Environment.Runtime, Is.SameAs(runtime));
				Assert.That(runtime.Environment.MessageBus, Is.SameAs(runtime.MessageBusCreated));
				Assert.That(MockSubsystem.OnLoadCalled, Is.EqualTo(5));
				Assert.That(MockSubsystem.OnCleanupCalled, Is.EqualTo(5));
				Assert.That(MockSubsystem.EnvironmentSupplied, Is.SameAs(runtime.EnvironmentCreated));

				Assert.Throws<InvalidOperationException>(() =>
					{
						runtime.Start();
					});
				Assert.Throws<InvalidOperationException>(() =>
					{
						runtime.Run();
					});

				runtime.Publish(new TestMessage());
				runtime.Publish("ABC", new TestMessage());
				runtime.PublishOperation(new TestOperation());
				runtime.PublishOperation("ABC", new TestOperation());

				runtime.Wait();
				Assert.That(runtime.Wait(10), Is.True);
				Assert.That(runtime.Wait(TimeSpan.FromMilliseconds(10)), Is.True);
			}
		}

		[Test]
		public void Test_Publish()
		{
			using (var runtime = new MockRuntime())
			{
				runtime.Start();

				var callbackCalled = 0;
				var callbackMessage = (Message)null;
				var callbackSignal = new AutoResetEvent(false);

				runtime.Environment.MessageBus.Subscribe(typeof(Message),
					msg =>
					{
						callbackCalled++;
						callbackMessage = msg;
						callbackSignal.Set();
					});

				var message = new TestMessage();
				runtime.Publish(message);

				callbackSignal.WaitOne(5000);
				Assert.That(callbackCalled, Is.EqualTo(1));
				Assert.That(callbackMessage, Is.SameAs(message));

				runtime.Shutdown();
				runtime.Wait();

				runtime.Publish(new TestMessage());

				callbackSignal.WaitOne(500);
				Assert.That(callbackCalled, Is.EqualTo(1));
			}
		}

		[Test]
		public void Test_Publish_Label()
		{
			using (var runtime = new MockRuntime())
			{
				runtime.Start();

				var callbackCalled = 0;
				var callbackMessage = (Message)null;
				var callbackSignal = new AutoResetEvent(false);

				runtime.Environment.MessageBus.Subscribe(typeof(Message),
					msg =>
					{
						callbackCalled++;
						callbackMessage = msg;
						callbackSignal.Set();
					},
					msg =>
					{
						return "AAA".Equals(msg.Label);
					});

				var message = new TestMessage();
				runtime.Publish("AAA", message);

				callbackSignal.WaitOne(5000);
				Assert.That(callbackCalled, Is.EqualTo(1));
				Assert.That(callbackMessage, Is.SameAs(message));

				runtime.Shutdown();
				runtime.Wait();

				runtime.Publish("AAA", new TestMessage());

				callbackSignal.WaitOne(500);
				Assert.That(callbackCalled, Is.EqualTo(1));
			}
		}

		[Test]
		public void Test_PublishOperation()
		{
			using (var runtime = new MockRuntime())
			{
				runtime.Start();

				var callbackCalled = 0;
				var callbackMessage = (Message)null;

				runtime.Environment.MessageBus.Subscribe<TestOperation>(
					msg =>
					{
						callbackCalled++;
						callbackMessage = msg;
						msg.Complete(None.Instance);
					});

				var message = new TestOperation();
				runtime.PublishOperation(message);

				Assert.That(message.Completion.Wait(5000), Is.True);
				Assert.That(callbackCalled, Is.EqualTo(1));
				Assert.That(callbackMessage, Is.SameAs(message));

				runtime.Shutdown();
				runtime.Wait();

				message = new TestOperation();
				runtime.PublishOperation(message);

				Assert.That(message.Completion.Wait(500), Is.False);
				Assert.That(callbackCalled, Is.EqualTo(1));
			}
		}

		[Test]
		public void Test_PublishOperation_CancellationToken()
		{
			using (var runtime = new MockRuntime())
			{
				runtime.Start();

				var callbackCalled = 0;
				var callbackMessage = (Message)null;

				runtime.Environment.MessageBus.Subscribe<TestOperation>(
					msg =>
					{
						callbackCalled++;
						callbackMessage = msg;
						msg.Complete(None.Instance);
					});

				var message = new TestOperation();
				var cancelSource = new CancellationTokenSource();
				runtime.PublishOperation(message, cancelSource.Token);

				Assert.That(message.Completion.Wait(5000), Is.True);
				Assert.That(callbackCalled, Is.EqualTo(1));
				Assert.That(callbackMessage, Is.SameAs(message));

				Assert.That(message.CancellationToken.IsCancellationRequested, Is.False);
				cancelSource.Cancel();
				Assert.That(message.CancellationToken.IsCancellationRequested, Is.True);

				runtime.Shutdown();
				runtime.Wait();

				message = new TestOperation();
				runtime.PublishOperation(message);

				Assert.That(message.Completion.Wait(500), Is.False);
				Assert.That(callbackCalled, Is.EqualTo(1));
			}
		}

		[Test]
		public void Test_PublishOperation_Label()
		{
			using (var runtime = new MockRuntime())
			{
				runtime.Start();

				var callbackCalled = 0;
				var callbackMessage = (Message)null;

				runtime.Environment.MessageBus.Subscribe<TestOperation>(
					msg =>
					{
						callbackCalled++;
						callbackMessage = msg;
						msg.Complete(None.Instance);
					},
					msg =>
					{
						return "AAA".Equals(msg.Label);
					});

				var message = new TestOperation();
				runtime.PublishOperation("AAA", message);

				Assert.That(message.Completion.Wait(5000), Is.True);
				Assert.That(callbackCalled, Is.EqualTo(1));
				Assert.That(callbackMessage, Is.SameAs(message));

				runtime.Shutdown();
				runtime.Wait();

				message = new TestOperation();
				runtime.PublishOperation("AAA", message);

				Assert.That(message.Completion.Wait(500), Is.False);
				Assert.That(callbackCalled, Is.EqualTo(1));
			}
		}

		[Test]
		public void Test_PublishOperation_Label_CancellationToken()
		{
			using (var runtime = new MockRuntime())
			{
				runtime.Start();

				var callbackCalled = 0;
				var callbackMessage = (Message)null;

				runtime.Environment.MessageBus.Subscribe<TestOperation>(
					msg =>
					{
						callbackCalled++;
						callbackMessage = msg;
						msg.Complete(None.Instance);
					},
					msg =>
					{
						return "AAA".Equals(msg.Label);
					});

				var message = new TestOperation();
				var cancelSource = new CancellationTokenSource();
				runtime.PublishOperation("AAA", message, cancelSource.Token);

				Assert.That(message.Completion.Wait(5000), Is.True);
				Assert.That(callbackCalled, Is.EqualTo(1));
				Assert.That(callbackMessage, Is.SameAs(message));

				Assert.That(message.CancellationToken.IsCancellationRequested, Is.False);
				cancelSource.Cancel();
				Assert.That(message.CancellationToken.IsCancellationRequested, Is.True);

				runtime.Shutdown();
				runtime.Wait();

				message = new TestOperation();
				runtime.PublishOperation("AAA", message);

				Assert.That(message.Completion.Wait(500), Is.False);
				Assert.That(callbackCalled, Is.EqualTo(1));
			}
		}

		[Test]
		public void Test_LoadSubsystem_Type_BeforeStart()
		{
			using (var runtime = new MockRuntime())
			{
				Assert.Throws<InvalidOperationException>(() =>
					{
						runtime.TestLoadSubsystem(typeof(MockSubsystem));
					});
			}
		}

		[Test]
		public void Test_LoadSubsystem_Type_AfterShutdown()
		{
			using (var runtime = new MockRuntime())
			{
				runtime.Start();
				runtime.Shutdown();

				Assert.Throws<InvalidOperationException>(() =>
					{
						runtime.TestLoadSubsystem(typeof(MockSubsystem));
					});
			}
		}

	}
}

#endif
